package uk.org.brindy.osgijc;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Manifest;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.FileList;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.FileList.FileName;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class OSGiJavac extends Javac {

	private Set<String> ignoreDotClasspathEntries = new HashSet<String>();

	public OSGiJavac() {
		ignoreDotClasspathEntries
				.add("org.eclipse.jdt.launching.JRE_CONTAINER");
		ignoreDotClasspathEntries.add("org.eclipse.pde.core.requiredPlugins");
	}

	private File baseDir;

	private Properties buildProperties;

	private String bundleDir;

	public void setBundleDir(String bundleDir) {
		this.bundleDir = bundleDir;
	}

	@Override
	public void execute() throws BuildException {
		System.out.println("OSGi Javac 1.0.2");

		// validate
		if (getSourcepath() != null) {
			throw new BuildException(
					"sourcepath is not supported.  srcdir and destdir "
							+ "are set exclusively by build.properties.");
		}

		if (getSrcdir() != null) {
			throw new BuildException("srcdir is not supported.  "
					+ "sourcepath is set from build.properties "
					+ "(\"source..\" variable).");
		}

		if (getDestdir() != null) {
			throw new BuildException("destdir is not supported. "
					+ "destdir is set from build.properties "
					+ "(\"output..\" variable).");
		}
		
		// examine the manifest and classpath entries (e.g. libs)
		baseDir = new File(bundleDir == null ? "." : bundleDir);
		try {
			debug("Base dir: " + baseDir.getCanonicalPath());
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		loadBuildProperties();
		debug("Build properties loaded");

		setSrcdirFromBuildProperties();
		debug("Srcdir set");
		
		setDestdirFromBuildProperties();
		debug("Destdir set");

		addToClasspathFromManifest();
		debug("Classpath configured by manifest");
		
		addToClasspathFromDotClasspathFile();
		debug("Classpath configured by .classpath");

		super.execute();
	}

	private void loadBuildProperties() {
		buildProperties = new Properties();
		File buildPropertiesFile = new File(baseDir, "build.properties");
		debug("buildPropertiesFile: " + buildPropertiesFile);
		
		FileInputStream in = null;
		try {
			in = new FileInputStream(buildPropertiesFile);
			debug("in: " + in);
			
			buildProperties.load(in);
			debug("build.properties loaded");
			
		} catch (FileNotFoundException e) {
			throw new BuildException("Cannot find file build.properties, check bundleDir attribute is set correctly.");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (null != in) {
					in.close();
				}
			} catch (IOException ex) {
			}
		}
				
	}

	private void setSrcdirFromBuildProperties() {
		String path = buildProperties.getProperty("source..");
		Path srcDir = extractPath(path);
		System.out.println("srcdir: " + srcDir);
		setSrcdir(srcDir);
	}

	private Path extractPath(String path) {
		String[] paths = path.split("\\,");

		Path srcPath = null == getSourcepath() ? new Path(getProject())
				: getSourcepath();
		FileList list = new FileList();

		for (String dir : paths) {
			FileName name = new FileList.FileName();
			File dirFile = new File(baseDir, dir.trim());
			try {
				name.setName(dirFile.getCanonicalPath());
			} catch (IOException e) {
				throw new BuildException(e);
			}
			list.addConfiguredFile(name);
		}

		srcPath.addFilelist(list);
		return srcPath;
	}

	private void setDestdirFromBuildProperties() {
		File destDir = new File(baseDir, buildProperties.getProperty("output.."
				.trim()));
		System.out.println("destdir: " + destDir);
		setDestdir(destDir);
	}

	private void addToClasspathFromManifest() {
		Manifest mf = getManifest(baseDir);

		String classpath = mf.getMainAttributes().getValue("Bundle-ClassPath");
		if (null == classpath) {
			System.out.println("No Bundle-ClassPath attribute in manifest");
		} else {
			FileList flist = new FileList();

			StringTokenizer strtok = new StringTokenizer(classpath, ",");
			while (strtok.hasMoreTokens()) {
				String entry = strtok.nextToken();
				File entryFile = new File(baseDir, entry);
				String entryName;
				try {
					entryName = entryFile.getCanonicalPath();
				} catch (IOException e) {
					throw new OSGiJavacException(e);
				}
				FileName fname = new FileList.FileName();
				System.out.println("Adding [" + entryName + "] to classpath");
				fname.setName(entryName);
				flist.addConfiguredFile(fname);
			}

			if (flist.size() > 0) {
				getClasspath().add(flist);
			}

		}
	}

	private void addToClasspathFromDotClasspathFile() {
		File dotClasspath = new File(baseDir, ".classpath");

		// parse the dotclasspath
		final Set<String> entries = new HashSet<String>();
		SAXParserFactory factory = SAXParserFactory.newInstance();
		try {
			// extract the classpaths

			SAXParser parser = factory.newSAXParser();
			parser.parse(dotClasspath, new DefaultHandler() {
				@Override
				public void startElement(String uri, String localName,
						String name, Attributes attributes) throws SAXException {
					if ("classpathentry".equals(name)) {
						if ("con".equals(attributes.getValue("kind"))) {
							entries.add(attributes.getValue("path"));
						}
					}
				}
			});
		} catch (ParserConfigurationException e) {
			throw new OSGiJavacException(e);
		} catch (SAXException e) {
			throw new OSGiJavacException(e);
		} catch (IOException e) {
			throw new OSGiJavacException(e);
		}

		// lookup the actual paths to the paths specified in the entries
		FileList flist = new FileList();

		for (String entry : entries) {

			if (!ignoreDotClasspathEntries.contains(entry)) {
				String classpathEntry = getProject().getProperty(entry);
				if (null == classpathEntry) {
					String msg = "Unknown classpath entry [" + entry
							+ "].  Use a property of the same name to "
							+ "link to a classpath element.";
					System.err.println(msg);
					throw new OSGiJavacException(msg);
				}

				File classpathEntryFile = new File(classpathEntry);
				if (!classpathEntryFile.exists()) {
					classpathEntryFile = new File(baseDir, classpathEntry);
					if (!classpathEntryFile.exists()) {
						String msg = "Classpath entry does not exist ["
								+ classpathEntry + "]";
						System.err.println(msg);
						throw new OSGiJavacException(msg);
					}
				}

				try {
					if (classpathEntryFile.isFile()) {
						String fnameString = classpathEntryFile
								.getCanonicalPath();
						FileName fname = new FileList.FileName();
						fname.setName(fnameString);
						System.out.println("Adding file [" + fnameString
								+ "] to classpath");
						flist.addConfiguredFile(fname);
					} else if (classpathEntryFile.isDirectory()) {
						System.out.println("Adding directory ["
								+ classpathEntryFile.getCanonicalPath()
								+ "] to classpath");
						DirSet dset = new DirSet();
						dset.setDir(classpathEntryFile);
						getClasspath().addDirset(dset);
					}
				} catch (IOException e) {
					throw new OSGiJavacException(e);
				}
			}

			if (flist.size() > 0) {
				getClasspath().addFilelist(flist);
			}

		}

	}

	private Manifest getManifest(File bundleDirFile) {
		File manifest = new File(bundleDirFile, "META-INF/MANIFEST.MF");
		System.out.println("Manifest:" + manifest);
		try {
			return new Manifest(new FileInputStream(manifest));
		} catch (IOException e) {
			try {
				throw new OSGiJavacException("Unable to read manifest ["
						+ manifest.getCanonicalPath() + "]");
			} catch (IOException e1) {
				throw new OSGiJavacException(e1);
			}
		}
	}

	private void debug(String message) {
		if (false) {
			System.out.println(message);
		}
	}

}
